You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Adds a Payjoin (BIP-77/BIP-78) section to SECURITY.md so future external reports can be triaged against documented, verifiable invariants instead of re-analyzed from scratch.
SECURITY.md: protocol invariants that are intentional design and should not be reported (receiver-held signed fallback, one proposal per URI, original/proposal mutual exclusivity) — each claim cites the exact BIP-77 section with verbatim quotes; sender-retry double-payment considerations (not conflict-bound, but exposure minimal in practice); the residual cross-URI risk at the UX layer; pinned dependency chain (payjoin 0.2.1 → rust-payjoin payjoin-1.0.0-rc.8).
AGENTS.md: new Security-section rule requiring SECURITY.md to be kept current with officially sourced, carefully reviewed notes when a security question is settled.
Motivated by a recent external report claiming a payjoin expiry/retry double-payment; analysis showed the mechanism does not hold against BIP-77 (single request/response per URI, mailbox expiry precedes any retry), and the residual risk is the re-issued-URI scenario at the social layer.
Review — PR #2569: payjoin invariants in SECURITY.md
Read the PR diff (SECURITY.md, AGENTS.md)
Verify BIP-77 citations and quotes (blocked — see note below)
Verify referenced code paths and expiry defaults (bull_payjoin)
Verify pinned dependency versions against pubspec.lock
Post review summary
Note: I can't submit formal inline review comments (GitHub App limitation), so findings are listed here with file/line references. I also couldn't fetch BIP-77 from this CI environment (WebFetch/curl/gh api to external repos are not in the allowed tools), so the verbatim quotes could not be machine-checked — see finding 3.
Findings
1. SECURITY.md:21 — "the receiver's mailbox has always expired too" is not always true (correctness)
The retries paragraph claims a retry is only possible after the sender session expires (default 24 h), "by which time the receiver's mailbox has always expired too — the retried original never reaches the receiver." The code does not support "always":
The sender session lifetime is min(requestedExpiry, policy.sessionLifetime) (packages/bull_payjoin/lib/src/engine/payjoin_runtime.dart:511-525, applied to senders at line 251), and policy.sessionLifetime is user-configurable from 1 minute to 24 h via the advanced settings screen (lib/features/settings/ui/screens/bitcoin/payjoin_advanced_settings_screen.dart:128-144). A user who lowers it to, say, 1 h gets an expired-and-retryable sender session while an external receiver's mailbox can still be live.
The pay/sell flows pass the remaining order-deadline window (lib/features/pay/presentation/pay_bloc.dart:607-625), which can likewise undershoot the receiver's mailbox lifetime.
Nothing sender-side ever reads the URI's EX: expireAfterSec is purely local (packages/bull_payjoin/lib/src/engine/pdk_payjoin_datasource.dart:212 falls back to PayjoinConstants.defaultExpireAfterSec). So there is no code-level ordering between sender expiry and mailbox expiry.
The conclusion ("at most one original is ever processed") still holds, but via the single-request receiver state machine documented in bullet 2 — not via expiry ordering. Since this document exists precisely to triage future reports, the justification should be the one that actually holds. Suggested rewording: scope the expiry argument to the default configuration ("at the default 24 h lifetime, the mailbox — created before the sender session started — has expired by retry time") and lean on bullet 2 for the general guarantee, noting the lifetime is user-configurable down to 1 minute. Fix this →
2. SECURITY.md:16 — "the directory mailbox expires at the URI's EX timestamp" (accuracy)
EX is a URI fragment parameter. Fragments are never transmitted to any server, so the directory cannot see EX; mailbox lifetime is directory-side policy, and EX is the receiver's advertised expiry to the sender. The argument the bullet makes still works (the sender must treat the session as dead at EX), but a spec-citing security doc shouldn't state a directory behavior the directory can't implement. Suggest: "the receiver advertises the session expiry via the URI's EX fragment parameter, after which the sender treats the session as expired."
3. Verbatim BIP-77 quotes could not be machine-verified (process)
The section quotes seven phrases and six section anchors from BIP-77 as verbatim. This environment has no network access to fetch bitcoin/bips (WebFetch and curl are outside the allowed tools — adding WebFetch to --allowedTools would let me do this next time), so please have a human spot-check the quotes and anchors before merge. Given the new AGENTS.md rule this PR introduces ("every claim must cite an official source… careful review before merging"), the founding example should hold itself to that bar — a paraphrase presented as a verbatim quote would undercut the whole mechanism.
Verified correct
Pinned versions: pubspec.lock:1503 has exactly payjoin 0.2.1+payjoin-1.0.0-rc.8, matching both links in SECURITY.md:13.
Both cited file paths exist and say what the doc claims: retry is gated on isExpired && !isCompleted && !isAborted (payjoin_engine.dart:294-302), and the not-conflict-bound behavior matches the engine's own design comments (payjoin_engine.dart:316-318 and 405-411 — "old and new payments need not conflict, so both can confirm").
The 24 h default is real (payjoin_policy.dart:7,41, payjoin_constants.dart:17).
The residual-risk paragraph (SECURITY.md:23, re-issued URI) matches the engine's threat model, and the AGENTS.md rule is clear and consistent with the existing Security section's style.
Summary
Docs-only PR, no code regressions possible. The structure and intent are good; the two substantive issues are in the justifications: line 21's expiry-ordering claim ("always expired too") doesn't hold under the user-configurable session lifetime or order-deadline windows, and line 16 attributes EX enforcement to the directory, which never sees the fragment. Both are worth fixing before merge precisely because this document's purpose is to be the authoritative triage reference. The verbatim BIP-77 quotes need a human spot-check since I had no network access.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a Payjoin (BIP-77/BIP-78) section to SECURITY.md so future external reports can be triaged against documented, verifiable invariants instead of re-analyzed from scratch.
Motivated by a recent external report claiming a payjoin expiry/retry double-payment; analysis showed the mechanism does not hold against BIP-77 (single request/response per URI, mailbox expiry precedes any retry), and the residual risk is the re-issued-URI scenario at the social layer.